Skip to content

feat(xlings): provision [xlings] deps on first build, at global scope - #531

Merged
Sunrisepeak merged 2 commits into
mainfrom
feat/xlings-subos-layering
Aug 29, 2026
Merged

feat(xlings): provision [xlings] deps on first build, at global scope#531
Sunrisepeak merged 2 commits into
mainfrom
feat/xlings-subos-layering

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 29, 2026

Copy link
Copy Markdown
Member

Scope note — "where is the SubOS environment half?"

It is already in mcpp, and has been since #352. subos_info.cppm parses the
envs block, runtime_binding.cppm collects it into binding.environment,
execute.cppm::compute_subos_env() injects it into mcpp run / mcpp test
children with ${subosdir} expanded and prepend applied — and
tests/e2e/200_subos_env_reaches_program.sh already asserts exactly that
("one selected RuntimeBinding snapshot must reach full-path and cached
mcpp run without an environment/CLI override").

Nothing about that was missing. What was missing is a payload in the
SubOS mcpp reads
, and that is this PR. The design note had also planned a
"layer the project SubOS over the toolchain SubOS" change (B3); measurement
showed it unnecessary once provisioning installs at the right scope, so it is
deliberately not here — see below.


[xlings] deps was declared and never installed. ensure_project_index_dir wrote it into .mcpp/.xlings.json verbatim and stopped there, so a manifest saying deps = ["xim:mesa"] produced a file naming mesa, no payload anywhere, and:

fatal error: gbm.h: No such file or directory

The declaration looked accepted and did nothing — the worst shape a config key can have. [toolchain] has had declare it and mcpp provisions it on first use all along (the "First run — no toolchain configured … installing … as default" block); a build environment should not have two grades of declaration.

Global scope, and the scope is the whole point

The obvious implementation — install_packages against make_project_xlings_env — installs at project scope, and measurably does not work. On a fresh MCPP_HOME the headers land in <proj>/.mcpp/.xlings/subos/_/usr/include while --sysroot names <MCPP_HOME>/registry/subos/default: two SubOS views, payload in the one the compiler does not read, #include <gbm.h> still failing with the dependency installed and declared.

make_xlings_env is the global env, so the payload lands in the registry whose SubOS is the sysroot — the same place [toolchain] installs into.

That single choice is what removes the need for any sysroot-layering machinery. Nothing in linkmodel.cppm, plan.runtimeSearch or link_line.cppm is touched — those carry ordering invariants whose own comments say "a mutable view that outranks either of them lets a later install silently change which library an ALREADY LINKED artifact loads. That is not hypothetical — it is the defect this module was created for."

Why install_packages and not resolve_xpkg_path

resolve_xpkg_path requires <name>@<version> and rejects a bare name — verified:

invalid xpkg target 'xim:mesa': expected `<name>@<version>`

A manifest is entitled to name a package without pinning it. install_packages resolves the version itself, and reports an ambiguous name with its candidates.

(Noted in passing: the toolchain path's (void)fetcher.resolve_xpkg_path(dep, …) for xim:glibc / xim:linux-headers discards its result, so the same rejection there would be silent. Not changed here.)

Declared deps only

Provisioning uses runtimeOwnerManifest.xlings.deps, not penv.deps. A cross-compilation target sysroot is appended to the latter a few lines up, and provisioning it would change behaviour for projects that never asked: a name that does not resolve would turn a build that used to proceed into a hard failure. The contract is what you declared gets installed; the sysroot entry is mcpp's inference, not the author's declaration.

Ordering is load-bearing

Provisioning runs before the runtime binding resolves, because a named [xlings] subos that does not exist yet is a hard error —

selected SubOS '_' does not exist at …; create/bootstrap that environment

— and provisioning is what creates it. Placed next to the custom-index sync, both first-use steps sit in one place.

Idempotent by content

A stamp records the dep list, so editing the list re-provisions and an unchanged list costs no xlings round-trip. Verified: a second mcpp run emits no Provisioning line.

Verified end to end on a fresh MCPP_HOME

A project with no mcpp-index dependency at all:

[xlings]
deps = ["xim:mesa"]

[build]
ldflags = ["-lgbm"]

src/main.cpp containing only #include <gbm.h> and a gbm_format_get_name call:

Provisioning [xlings] deps (xim:mesa)
   Compiling nopkg v0.1.0 (.)
     Running `target/.../bin/nopkg`
XR24 | GBM_BACKENDS_PATH=<registry>/subos/default/usr/lib/gbm

Compile, link, run and environment all close — the environment via the pre-existing compute_subos_env path, now that there is something in the SubOS for it to read. The GBM_BACKENDS_PATH declaration itself comes from openxlings/xim-pkgindex#713; before that PR the same run leaves it unset while everything else works.

Tested with bare (xim:mesa) and pinned (xim:mesa@25.0.7.2) spellings.

Tests

mcpp test96 passed, 0 failed (1 skipped: the non-Linux boundary, as always on a Linux runner). The SubOS-env axis keeps its existing coverage in tests/e2e/200_subos_env_reaches_program.sh.

Design note: mcpp-index .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md §9 and §12 — §12.1 records the three measurements that retired B3.

Sunrisepeak added a commit to mcpplibs/mcpp-index that referenced this pull request Aug 29, 2026
§11 splits the work into T1–T10 with dependencies, and evaluates it against
architecture / stability / simplicity / UX / compatibility / cross-platform /
consistency / seamless-upgrade / test-coverage. Key structural point: R1
(xim-pkgindex) and R2 (mcpp) are independent chains, so the GBM closed loop
does not wait on the larger mcpp work.

§12 records what implementation actually found, and it overturns §8's central
conclusion. B3 (layer the project SubOS over the toolchain SubOS) is NOT
needed. The fix is to provision `[xlings] deps` at GLOBAL scope, because that
registry's SubOS *is* mcpp's `--sysroot`; once the payload lands there, headers
and libraries are visible with no -isystem/-L overlay at all. Three
measurements got there:

  * project scope  -> installs fine, headers land in the SubOS the compiler
                      does not read, gbm.h still not found
  * resolve_xpkg_path (global) -> headers reach the sysroot, but it demands
                      <name>@<version> and rejects a bare name
  * install_packages + make_xlings_env (global) -> correct for bare,
                      namespaced and pinned spellings alike

So §8.1's "the project SubOS lacks libgcc_s/libstdc++" table is still fact; it
just proves "do not install there" rather than "layer over it". The data was
right and the conclusion was backwards. This also keeps the change an order of
magnitude smaller — nothing touches linkmodel.cppm, plan.runtimeSearch or
link_line.cppm, whose comments document exactly the defect that reordering a
mutable view would reintroduce.

Verification recorded in full: the real ecosystem run under
`xlings subos use --sandbox --gpu` allocating an actual gbm buffer object on
card0, and the fresh-MCPP_HOME mcpp run closing compile/link/run/env with zero
mcpp-index packages. Also the one thing still open and out of scope — the
xim-x-mesa payload whose RUNPATH names glibc 2.39 while its own libgallium
needs GLIBC_2.43 — and two verification traps worth knowing (MCPP_HOME appends
another `registry/`, and mcpp keeps its own index copy separate from
~/.xlings).

PRs: openxlings/xim-pkgindex#713 (C1), mcpp-community/mcpp#531 (R2b).
`[xlings] deps` was DECLARED and never installed. `ensure_project_index_dir`
wrote it into `.mcpp/.xlings.json` verbatim and stopped there, so a manifest
saying `deps = ["xim:mesa"]` produced a file naming mesa, no payload anywhere,
and `fatal error: gbm.h: No such file or directory`. The declaration looked
accepted and did nothing, which is the worst shape a config key can have —
`[toolchain]` has had "declare it and mcpp provisions it on first use" all
along ("First run — no toolchain configured … installing … as default"), and a
build environment should not have two grades of declaration.

GLOBAL SCOPE, AND THE SCOPE IS THE WHOLE POINT. The obvious implementation —
`install_packages` against `make_project_xlings_env` — installs at PROJECT
scope, and measurably does not work. On a fresh MCPP_HOME the headers land in
`<proj>/.mcpp/.xlings/subos/_/usr/include` while `--sysroot` names
`<MCPP_HOME>/registry/subos/default`: two SubOS views, payload in the one the
compiler does not read, `#include <gbm.h>` still failing with the dependency
installed and declared. `make_xlings_env` is the global env, so the payload
lands in the registry whose SubOS *is* the sysroot — the same place
`[toolchain]` installs into. That single choice is what removes the need for
any sysroot-layering machinery: a project dep and a toolchain dep now agree on
where they live, so one `--sysroot` sees both.

`install_packages` rather than `resolve_xpkg_path`: the latter requires
`<name>@<version>` and rejects a bare `mesa` (verified: "invalid xpkg target
'xim:mesa': expected `<name>@<version>`"), while a manifest is entitled to name
a package without pinning it. install_packages resolves the version itself and
reports an ambiguous name with its candidates, which is an error the author can
act on.

ORDER IS LOAD-BEARING: provisioning runs BEFORE the runtime binding resolves,
because a named `[xlings] subos` that does not exist yet is a hard error
("selected SubOS '…' does not exist; create/bootstrap that environment") and
provisioning is what creates it. Placed next to the custom-index sync, both
first-use steps sit in one place.

Idempotent by CONTENT, not existence: a stamp records the dep list, so editing
the list re-provisions and an unchanged list costs no xlings round-trip.
Verified — a second `mcpp run` emits no Provisioning line.

VERIFIED end to end on a FRESH MCPP_HOME, with a project that has no
mcpp-index dependency at all:

    [xlings]
    deps = ["xim:mesa"]
    [build]
    ldflags = ["-lgbm"]

    Provisioning [xlings] deps (xim:mesa)
    Compiling nopkg v0.1.0 (.)
    Running `target/.../bin/nopkg`
    XR24 | GBM_BACKENDS_PATH=<registry>/subos/default/usr/lib/gbm

`#include <gbm.h>` compiles, `-lgbm` links, and the SubOS env declaration
reaches the process — the last of those needs openxlings/xim-pkgindex#713,
which adds GBM_BACKENDS_PATH to the graphics discovery table.

Design: mcpp-index .agents/docs/2026-08-30-gbm-cross-repo-closed-loop-plan.md
`[xlings] deps` is manifest input and the arguments were assembled by
formatting the strings into a JSON literal, so a dependency name containing a
quote or a backslash would emit malformed JSON. The failure would then surface
as an xlings parse error naming neither the manifest nor the key that caused
it.

nlohmann::json is already imported in this translation unit (mcpp.libs.json),
so this is `args["targets"] = declaredDeps; args["yes"] = true; args.dump()`
and the escaping stops being something a reader has to verify by eye.

No behaviour change for well-formed names, which is every name in practice --
this is about the failure mode of the one that is not.
@Sunrisepeak
Sunrisepeak merged commit ab1da5d into main Aug 29, 2026
36 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/xlings-subos-layering branch August 29, 2026 19:11
Sunrisepeak added a commit to mcpplibs/mcpp-index that referenced this pull request Aug 29, 2026
…m's Mesa (#281)

* feat(libgbm): add compat.libgbm 2026.08.29, GBM bound to the ecosystem's Mesa

GBM is the API a program uses to get scanout-capable buffers out of a DRM
device — gbm_device, gbm_bo, gbm_surface. It sits under EGL on a KMS
console, under a compositor's back end, and under headless GPU rendering.

Shape I, new: an ECOSYSTEM-STACK BINDING. Not a source build, and the
reason is a dependency-surface argument rather than a convenience one.

  * Upstream ships no separable unit. `src/gbm/meson.build` is
    `link_with: [libloader]`, and libloader wants `idep_mesautil` — the
    whole of Mesa's internal util library, ~120 TUs plus Python-generated
    tables — for exactly ONE function, loader_open_driver_lib; plus
    -DUSE_DRICONF (expat), libdrm, xcb, xcb-randr. GBM's frontend/backend
    dlopen split exists so vendors can ship BACKENDS, not so third parties
    rebuild the frontend. (compat.vulkan is not a precedent the other way:
    Khronos releases the loader as a standalone project; Mesa does not.)

  * In this ecosystem Mesa already has an owner, `xim:mesa`. A source
    build would make the index re-import libdrm + expat + xcb + a
    Mesa-util carve-out to duplicate a graph the ecosystem has already
    resolved — growing the surface to shrink nothing.

Measured surface: host 0, ecosystem 1 (xim:mesa, not xim:graphics's 22),
index 0 (deps = {}), transitive 0 — libgbm.so.1's own RUNPATH resolves
entirely inside xim-x-*.

ZERO HOST, with no escape hatch. Stricter than either neighbour on
purpose: glx-runtime keeps MCPP_HOST_GL_LIBRARY_PATH and vulkan-runtime
harvests /usr/lib outright, both because a proprietary vendor driver can
only come from the host. GBM has no such case, and host libgbm is a leak
the ecosystem already closed — xim:nvidia-gl-host-link names it: "the
table … was missing libm, libdrm, libgbm, libgcc_s … all of which were
therefore coming from the HOST, silently, which is the leak this package
exists to close (R7)". NVIDIA's own GBM backend, if ever wanted, belongs
in that host-link layer.

The part that is actual work: the backend is unreachable in the sandbox.
libgbm is a loader and Mesa compiles /usr/lib/gbm in as its search path,
which does not exist there —

    MESA-LOADER: failed to open dri: /usr/lib/gbm/dri_gbm.so: cannot open
    shared object file (search paths /usr/lib/gbm, suffix _gbm)

and xim:mesa declares `lib` into the view while `lib/gbm/` is a
subdirectory that does not follow. So install() also harvests the
backends, as a SIBLING of the farm's libgbm, and a generated TU derives
the path at runtime: dlsym(RTLD_DEFAULT) a gbm symbol, dladdr, append
"/gbm". Verified dladdr reports the FARM path, not the realpath, so the
sibling lands in this package's own payload and nothing is pinned —
unlike baking an absolute path into a generated header, which would fix
the package to whichever mesa payload existed on install day.

Two mechanism findings, both now in the docs:

  * runtime.library_dirs renders as -Wl,-rpath and NOT as -L; the -L key
    is runtime.link_library_dirs (added 2026.8.10.3), and
    transitive_needed_dirs is -Wl,-rpath-link. The catalog and
    package-types both asserted library_dirs joined the link line, which
    mcpp#304 did observe but the pinned mcpp no longer does. With
    library_dirs alone the farm is complete, the rpath right, and the
    build dies at `ld: cannot find -lgbm`. Corrected in all four docs.
  * c_standard = "gnu11" is still silently ignored, so dladdr/RTLD_DEFAULT
    come from cflags -D_GNU_SOURCE (the compat.libaio finding).

Target is gbm_binding, not gbm: a target named gbm would put a libgbm.a
beside the real libgbm.so and let search order decide -lgbm.

Test member asserts what is checkable without a GPU — 19 checks, all
green on a runner with no /dev/dri. The two legacy enumerators are the
load-bearing ones: GBM_BO_FORMAT_XRGB8888 is the value 0, and only the
library's own format_canonicalize() turns it into "XR24", so a
header-only reimplementation would pass the fourcc cases and fail these.
Backend reachability is asserted as PRESENCE at the derived path rather
than a successful dlopen, which stays honest on a host where the stack's
own mesa/glibc skew breaks the load. Device creation is opt-in behind
MCPP_RUN_GBM_DEVICE=1.

CN mirror published at gitcode mcpp-res/libgbm, fetched back and
confirmed byte-identical to GLOBAL. Verified with the CI-pinned mcpp
(2026.8.27.2): `mcpp test -p libgbm` green from cold, all lint gates and
`mcpp xpkg parse` clean across all 136 descriptors, and the assertions
confirmed failable — removing dri_gbm.so from the farm turns the
reachability check red and the binary exits 1.

* fix(libgbm): make the backend repair invisible — stock <gbm.h> is the API

The first cut exposed `mcpp_gbm.h` and asked the consumer to call
`mcpp_gbm_use_sibling_backends()` before creating a device. That changed
the ordinary way to use libgbm, and it was not only a style problem: it
does not work for the consumers that matter.

libgbm is mostly called from INSIDE other libraries — SDL2's KMSDRM
backend, wlroots, ffmpeg's VAAPI hwcontext all call gbm_create_device()
from their own sources. None of them will ever call a helper of ours, so
an opt-in repair leaves exactly those callers as broken as they were,
while the package's own tests go green.

The API is now stock `#include <gbm.h>` and nothing else. GBM_BACKENDS_PATH
is wired from a CONSTRUCTOR in the package's own TU (priority 101, ahead of
default-priority constructors in case one creates a device). An inherited
value is still left alone — this is a default, not an override.

That is reliable because a dependency's objects enter the consumer's link
eagerly rather than being lazily selected; the emitted build.ninja names
the object on the link line directly, so the constructor cannot be dropped.

It is also what every other ecosystem does, and none of them use an API:
distros (Debian libgbm1, Fedora mesa-libgbm) split libgbm out of the mesa
SOURCE package so the compiled-in $libdir/gbm is right by construction;
relocated and sandboxed stacks set the environment variable instead —
Valve's pressure-vessel hit this exact bug when mesa 24.3 split the
backends out (steam-runtime#797) and answers with GBM_BACKENDS_PATH,
as do Nix, Conda and AppImage at activation time; and Mesa offers
-Dgbm-backends-path= for packagers who control the build. This package is
in the sandboxed case and cannot set a container-wide environment, so the
constructor is the in-process equivalent. Longer term the wiring belongs
in xim:mesa (build with -Dgbm-backends-path=, or declare lib/gbm/ into the
view as it already declares lib and include), and then this package would
carry no constructor at all; noted in the design doc.

mcpp_gbm.h stays, demoted to optional introspection for diagnostics and
for the tests, and says so in its own first line.

Tests now come in two binaries, and the split is the regression guard for
this very mistake:

  * tests/stock_usage.cpp includes STOCK <gbm.h> and nothing else — no
    mcpp_gbm.h, no helper declaration. If the repair ever goes back to
    being opt-in, this fails while the fuller gbm.cpp could still pass.
  * tests/gbm.cpp reads GBM_BACKENDS_PATH before calling anything at all,
    and re-execs itself with the variable preset to prove an inherited
    value survives the constructor — the only way to observe that rule,
    since by the time main runs the constructor is finished.

Also drops the generated_files copy of the TU: install() is the only
writer, and the parser takes literals only so the two could not share one
source. Verified cold with the CI-pinned mcpp: 2 passed, 0 failed; both
binaries confirmed failable by removing dri_gbm.so from the farm.

* docs(libgbm): cross-repo closed-loop plan for GBM (mcpp / xim-pkgindex / mcpp-index)

Records what the PR #281 discussion established: making gbm_create_device()
work in an mcpp project needs no new mechanism. Every layer already exists
and runs; two wiring points are missing, in two different repos.

R1 (xim-pkgindex): GBM_BACKENDS_PATH is absent from graphics.lua's DISCOVERY
table. DRI and EGL vendor dirs are there; GBM is the same class of thing
(dlopen'd by path, not a link target) and simply never got its counterpart.

R2 (mcpp): the default runtime selection reads <xlingsHome>/subos/default,
whose envs is {}, while a project's xim: deps declare into
<proj>/.mcpp/.xlings/subos/_. Measured: with [xlings] subos = "_" the whole
chain works -- ${subosdir} expansion, prepend merge, child injection. So
mcpp#352 fixed HOW to inject and not WHERE to read from. R2 is not
gbm-specific: LIBGL_DRIVERS_PATH is unset too, so any mcpp-built GL program
currently cannot find a DRI driver.

Carries the measured evidence, the code locations, per-repo diffs, a
verification matrix whose V5 is the mechanical precondition for deleting
compat.libgbm's constructor, and the Conan/distro evidence for why the
package should stay independent but thin (split axis is the INTERFACE --
gbm.pc vs gl.pc -- not the source project; Conan has no gbm recipe at all
and models this class as <name>/system virtual packages).

* docs(libgbm): second-round self-review supersedes the mcpp-side proposal

Four substantive errors in the first cut, three of them 'concluded without
running the experiment'.

1. B1/B2 was a false dichotomy. Measured the project subos: it has libc,
   crt1.o, libm and ld-linux but NOT libgcc_s or libstdc++, so it is not a
   superset of the toolchain subos. B2 (switch selection) would therefore
   REGRESS, which is the real objection -- the one I gave (blast radius /
   full rebuild) was weak, since contractHash already handles invalidation.
   And B1 (merge envs only) fixes runtime while leaving gbm.h and -lgbm to
   the package forever. The correct shape is layered inheritance (B3):
   keep --sysroot on the toolchain subos and overlay the project subos as
   -isystem / -L / -rpath plus a prepend env merge. One compiler takes one
   --sysroot, so 'inherit' necessarily means base + overlay, not swap.

2. Never tested whether the compat package could be avoided entirely.
   It cannot, but not for the reason given: [xlings] deps materializes
   "deps": ["mesa"] into .mcpp/.xlings.json and then installs nothing
   (gbm.h not found, no subos created), and [xlings] subos errors rather
   than bootstrapping a missing subos. So a package's xpm.deps.runtime is
   currently the ONLY door into the xim layer for an mcpp project --
   which is itself a gap, and means compat.libgbm is presently doing a job
   that is not a library package's to do.

3. The verification matrix conflated the two C2 variants: V5 can only go
   green under B3. Under B1 the thin shim is permanent, not transitional.

4. R2 was one line where it is three: R2a ([xlings] subos cannot bootstrap),
   R2b ([xlings] deps materialized but not provisioned), R2c (default
   selection reads only the toolchain subos, no layering).

R1/C1 and the industry argument in §3 are unaffected.

* docs(libgbm): R2b fix (auto-provision [xlings] deps) + third-round review

R2b: [xlings] deps is materialized into .mcpp/.xlings.json and then nothing
installs it. mcpp already has two 'declare -> auto-install' paths to mirror:
the toolchain first-run flow (prepare.cppm ~1690, fetcher.resolve_xpkg_path
with autoInstall=true) and the project-scope install_packages capability
(~2936, which already carries the live progress UI and whose install
destination is chosen by package scope, so it lands in the project scope --
which is exactly what creates the project subos). Proposal reuses the
latter, keyed off penv.deps, idempotent, with a toolchain-shaped error that
prints the manual equivalent.

Ordering matters and subsumes R2a: provisioning must run BEFORE runtime
selection, or 'selected SubOS does not exist' fires first. So R2a is better
understood as 'provision before select' than as 'subos must bootstrap', and
[xlings] subos can keep its strict select-only semantics.

Third-round review corrects an expectation the earlier rounds got wrong:
even with R1 + R2b + B3 all landed, compat.libgbm does NOT disappear. The
[xlings] deps route only works for an application's OWN manifest, and GBM's
real consumers are mostly LIBRARIES (SDL2's KMSDRM backend, wlroots, ffmpeg
VAAPI) which cannot inject [xlings] deps into their consumer's manifest --
they can only declare a dependency edge. Same reason Conan ships
opengl/system as a package rather than telling users to write
system_requirements themselves. Also records four assumptions still
unverified, the sharpest being that B3's overlay must rank BELOW a package's
own include_dirs or it would swap out everyone's GL headers (the overlap
compat.glx-headers already documents as a real trap).

* docs(libgbm): task breakdown, and the measurements that killed B3

§11 splits the work into T1–T10 with dependencies, and evaluates it against
architecture / stability / simplicity / UX / compatibility / cross-platform /
consistency / seamless-upgrade / test-coverage. Key structural point: R1
(xim-pkgindex) and R2 (mcpp) are independent chains, so the GBM closed loop
does not wait on the larger mcpp work.

§12 records what implementation actually found, and it overturns §8's central
conclusion. B3 (layer the project SubOS over the toolchain SubOS) is NOT
needed. The fix is to provision `[xlings] deps` at GLOBAL scope, because that
registry's SubOS *is* mcpp's `--sysroot`; once the payload lands there, headers
and libraries are visible with no -isystem/-L overlay at all. Three
measurements got there:

  * project scope  -> installs fine, headers land in the SubOS the compiler
                      does not read, gbm.h still not found
  * resolve_xpkg_path (global) -> headers reach the sysroot, but it demands
                      <name>@<version> and rejects a bare name
  * install_packages + make_xlings_env (global) -> correct for bare,
                      namespaced and pinned spellings alike

So §8.1's "the project SubOS lacks libgcc_s/libstdc++" table is still fact; it
just proves "do not install there" rather than "layer over it". The data was
right and the conclusion was backwards. This also keeps the change an order of
magnitude smaller — nothing touches linkmodel.cppm, plan.runtimeSearch or
link_line.cppm, whose comments document exactly the defect that reordering a
mutable view would reintroduce.

Verification recorded in full: the real ecosystem run under
`xlings subos use --sandbox --gpu` allocating an actual gbm buffer object on
card0, and the fresh-MCPP_HOME mcpp run closing compile/link/run/env with zero
mcpp-index packages. Also the one thing still open and out of scope — the
xim-x-mesa payload whose RUNPATH names glibc 2.39 while its own libgallium
needs GLIBC_2.43 — and two verification traps worth knowing (MCPP_HOME appends
another `registry/`, and mcpp keeps its own index copy separate from
~/.xlings).

PRs: openxlings/xim-pkgindex#713 (C1), mcpp-community/mcpp#531 (R2b).

* docs(libgbm): delivery status, one PR per repo, and where the SubOS-env half lives

Records T1-T10 against the three PRs, and answers the review question this
plan invited: PR mcpp#531 contains only provisioning because the SubOS
ENVIRONMENT half was never missing from mcpp. subos_info.cppm parses envs,
runtime_binding collects them, execute.cppm::compute_subos_env injects them
with ${subosdir} expanded and prepend applied, and
tests/e2e/200_subos_env_reaches_program.sh has asserted exactly that since
mcpp#352.

What was missing was the GBM row in the declaration (T1, xim-pkgindex#713)
and a payload in the SubOS mcpp reads (T3, mcpp#531). T4 is retired outright
per §12.1.

mcpp#352 fixed HOW to inject; T1 supplies WHAT to inject; T3 supplies
something to read it from.

* docs(libgbm): should mesa (or its separable libraries) become mcpp-index packages?

mcpp does support shared-library packages -- package-types.md shape F,
compat.x11 and linux compat.vulkan -- so this is a should, not a can.

mesa itself: no. Two libgbm.so.1 in one process with xim:mesa already
providing one (compat.vulkan-runtime reached the same conclusion for
libvulkan: 'one loader per process is the whole point'); and mixing a shared
closure into an index whose compat.* are all static is a measured silent
symbol hijack -- 86 zlib symbols exported from the exe, libgio's 12 zlib
calls all bound there, the bundled libz.so.1 fully shadowed, running fine
with no warning. vcpkg forbids that with triplets and Conan with a
propagated shared option; mcpp/xlings has no such whole-graph switch.

But the instinct is right for the separable pieces, and the criterion is the
one from section 3: does upstream ship it as a separable unit. libdrm does,
Conan has a real recipe for it, and compat.vulkan-runtime currently harvests
libdrm*.so.* from the HOST -- exactly the host edge this plan exists to
close. A source-built compat.libdrm would close it and trips none of the
mesa objections. Recorded as the recommended next package.

* docs(libgbm): the constructor's removal condition is now mechanical

The descriptor said the real fix 'is worth filing'. It is filed and
implemented: openxlings/xim-pkgindex#713 puts GBM_BACKENDS_PATH in the
graphics discovery table, so xim:mesa declares it into the subos and every
consumer inherits it (measured: '4 env var(s) from 1 package(s)' where it was
3, and a real gbm_bo_create on card0).

So the comment now names the removal CONDITION instead of an intention, and
the condition is checkable rather than a judgement call: delete the TU, the
lib/gbm farm and mcpp_gbm.h, re-run tests/stock_usage.cpp -- which includes
stock <gbm.h> and nothing else -- and if it stays green the ecosystem is
supplying the value.

Also records that it is NOT green yet as of today: the value only arrives in
a home whose installed xim:mesa was configured by an index carrying #713,
i.e. after that PR merges and the artifact republishes. Verified again after
the edit: parse OK, 2 passed via the CN mirror, cold.

* docs(libgbm): reading guide — the doc records three rounds, last one wins

The plan now supersedes itself twice (section 2's C2 by section 8, section 8's
B3 by section 12.1), and a reader going top-down would act on retired advice.
A short guide up front says which sections are authoritative and which are
kept only as a record of reasoning that was overturned — worth keeping,
because what overturned each round was a measurement, and those measurements
are the durable part.

Also states the honest pattern: the only thing that kept changing was the
mcpp-side shape, and that is exactly the part I did not read the source or
run an experiment on before proposing.

* docs(libgbm): why the xlings pin stays at 2026.8.27.5

The task asked to pin the internal xlings dependency to 2026.8.27.4, "which
should be released by then". Checked, and the premise is inverted: .4 is an
OLDER already-published release (2026-08-27T10:18) while .5 is Latest
(2026-08-27T13:29, 8 assets), and every pin is already on .5 --
check_version_pins.sh reports "OK: xlings pins all at 2026.8.27.5", and
xim-pkgindex's xlings.lua already has latest = 2026.8.27.5. mcpp-index pins
mcpp, not xlings. So the intent -- pin to a released xlings -- is already
satisfied, and satisfied better.

Downgrading would also drop a property the source documents: .5 makes the
declaration outrank the index during resolution, so it holds even when
"latest" is not the highest entry in the table; .4 does not. The same comment
records what the pin is a FLOOR against: below 2026.8.27.2 the bundled xlings
takes a subos runtime binding from a compiled-in constant, so a home can
declare one glibc and install another, and mcpp is the party that fails.
kXlingsVersion is the single source of truth for every pin under .github/, so
a downgrade would touch release/CI/bootstrap in a dozen places.

The local warning that probably prompted this -- "vendored xlings 2026.8.27.4
is older than the pinned 2026.8.27.5, but no newer source is available" -- is
about the copy bundled into the mcpp release tarball, not about .5 being
unavailable; .5 has 8 downloadable assets. That message names its own fix,
which is to self-update xlings rather than to move mcpp's pin backwards.

Not executed; recorded in section 15 instead.

* docs(libgbm): mark section 8.1 superseded at its own heading

Section 2's C2 already carried a supersession banner; section 8 did not, so a
reader landing there directly would act on the retired B3 conclusion. The
reading guide said so, but a guide only helps someone who started at the top.

Scoped to 8.1 rather than the whole section: 8.2-8.6 (the untested-alternative
finding, the V-matrix correction, the R2a/R2b/R2c split) all still stand, and
8.1's own measurement is still fact -- it just proves "do not install into the
project scope" rather than "layer over it".

* docs(libgbm): V5 passes — the constructor is removable once #713 lands

Section 4 defined deleting the constructor as a mechanical precondition
rather than a judgement call. Simulated the post-merge state locally (copied
#713's graphics.lua and mesa.lua into ~/.mcpp/registry's index copy, re-ran
xlings install so mesa's config() re-declared) and then walked the REAL
dependency path -- [dependencies.compat] libgbm, no [xlings], no ldflags,
just #include <gbm.h> -- cold over the CN mirror:

    GBM_BACKENDS_PATH = /home/speak/.mcpp/registry/subos/default/usr/lib/gbm

That is the SUBOS path, not the package's own farm. The constructor is
`if (getenv("GBM_BACKENDS_PATH")) return;`, so a subos-shaped value proves
the ecosystem set it first and the constructor was a no-op. Against an
unpatched index the same path yields the in-package farm value; the only
variable between the two runs is whether the index carries the DISCOVERY row.

Not deleting yet, and the reason is the same gate working as designed: until
#713 merges and the artifact republishes, consumers on the published index
would lose the variable and tests/stock_usage.cpp would go red in CI -- which
is precisely the mechanical check, enforced rather than remembered.

Section 16.4 lists the exact one-step follow-up, and notes that
stock_usage.cpp stays: after the removal it stops asserting "our constructor
ran" and starts asserting "the whole ecosystem loop works", which is the most
valuable regression this package has.

* docs(libgbm): correct section 14.2 — compat.libdrm does not close that host edge

Section 14.2 recommended compat.libdrm on the grounds that it would close
compat.vulkan-runtime's host harvest of libdrm*.so.*. That reasoning is
wrong, and chasing it found a better next step.

vulkan-runtime's farm exists because the dlopen'd ICD ITSELF comes from the
host and carries its own DT_NEEDED (libdrm, LLVM, xcb), which must resolve
through the same directory. That needs host-compatible SHARED libraries. An
in-index compat.libdrm would be a static package like every other compat.*,
and a static archive cannot satisfy a .so's DT_NEEDED. I had conflated "there
is a package called libdrm" with "the farm needs libdrm*.so.*"; they are not
the same thing. libdrm may still be worth adding for build-time consumers,
but not for that reason.

The real next step is one layer up. xim:mesa already ships
share/vulkan/icd.d/radeon_icd.x86_64.json and lib/libvulkan_radeon.so, and
mesa.lua already calls graphics.declare_vulkan_icd(); with DISCOVERY's
XDG_DATA_DIRS the loader finds it. Meanwhile compat.vulkan-runtime still has
deps = {} and still sweeps /usr/lib/x86_64-linux-gnu -- which is exactly where
compat.glx-runtime stood before 2026.08.08, and glx-runtime's fix is the
template: depend on the ecosystem stack, keep the host door only for vendors
the ecosystem does not cover.

Stated honestly: the ecosystem's Vulkan coverage is AMD-only today (mesa.lua
says anv and NVK are still absent), so vulkan-runtime cannot reach the zero-host
position compat.libgbm reached. It should become ecosystem-first with a host
fallback, rather than host-only as it is now.

* fix(libgbm): the xpm anchor is inert — stop naming it like the header

Asked whether libgbm-2026.08.29.h actually needs downloading, or whether the
runtime payload already supplies the header. The payload supplies it: install()
symlinks gbm.h out of <subos>/usr/include and libgbm.so* out of <subos>/lib,
and the downloaded file is never read.

The anchor exists only because the xpm schema wants a url + sha256 per version
-- the same inert-anchor trick compat.glx-runtime plays with an
OpenGL-Registry README and compat.vulkan-runtime with a Vulkan-Loader README.

It used to be Mesa's own src/gbm/main/gbm.h, on the theory that the anchor may
as well record which header the package was written against. That was a
mistake: an anchor NAMED like the header this package installs reads as though
the download is the shipped header, which is the one thing it is not -- and the
first person to read the descriptor asked exactly that question. A README
cannot be mistaken for a payload, so the anchor is now Mesa's README.rst at the
same tag.

CN asset published alongside (gtc, mcpp-res/libgbm@2026.08.29), re-fetched and
compared byte-for-byte against GLOBAL: 03f0fd62... , 1720 bytes. sha256 taken
twice before use. Version deliberately NOT bumped: 2026.08.29 has never been
published from this index (the package is still in PR), so no consumer can
have resolved the old anchor.

Verified cold on both mirrors: MIRROR=CN and MIRROR=GLOBAL each 2 passed.

* feat(libgbm): the package sheds its workaround; index floor corrected

Two things, both found by reviewing the PR against the ecosystem rather than
against itself.

1. THE PACKAGE IS NOW WHAT IT SHOULD ALWAYS HAVE BEEN. 598 -> 303 lines.

openxlings/xim-pkgindex#713 merged and the index artifact republished, so
xim:mesa now declares GBM_BACKENDS_PATH into the subos and every consumer
inherits it. Everything this package carried to compensate is gone: the
constructor TU, the lib/gbm backend farm, mesa_libdir(), and mcpp_gbm.h.

What remains is a binding and nothing else -- deps = { runtime = xim:mesa },
an install() that symlinks gbm.h and libgbm.so* out of the subos view, and
include_dirs / ldflags / runtime dirs. It compiles no upstream source, ships no
header of its own, and sets no environment variable. Setting GBM_BACKENDS_PATH
was always Mesa's own mechanism and the environment's job; the package doing it
was the workaround, not the design.

tests/gbm.cpp drops the mcpp_gbm.h include and the re-exec test, and now
asserts the ECOSYSTEM supplies the path -- which makes this repo's CI the
tripwire on xim-pkgindex's DISCOVERY row and on mcpp's subos-env injection.

2. THE INDEX FLOOR WAS FALSE, and this package proved it.

index.toml claimed min_mcpp = 2026.8.3.3. Measured against that version:
compat.libgbm reports `parse OK` and then fails to link --

    libdrm.so.2, needed by .../libgbm.so, not found
    undefined reference to `drmGetVersion'

because 2026.8.3.3 does not know runtime.link_library_dirs (the string does not
occur in the binary) and SILENTLY IGNORES it. Silently-ignored keys are
invisible to the lint, so the "floor first, new grammar after" guard could not
catch this: it assumes unknown keys are rejected, and runtime.* subkeys are not.

2026.8.10.3 does not even get that far -- it cannot bootstrap against this
index at all ("selected RuntimeBinding glibc@2.44 requires payload ... but it
is not installed"), which is the compiled-in-binding defect mcpp's own
xlings.cppm cites as its reason to floor xlings at 2026.8.27.2.

So every client below 2026.8.27.2 was already broken here for reasons
predating this package. Floor and latest raised to 2026.8.27.2 -- the version
validate.yml has pinned all along, which also restores the "move it with the CI
pin" invariant that had quietly drifted.

Verified: `mcpp test -p libgbm` 2 passed cold via the CN mirror, against an
index synced from the PUBLISHED artifact (xlings update) rather than a
hand-patched copy; all lint gates and mcpp xpkg parse clean across 136
descriptors.

* docs(libgbm): the shipped docs still described the deleted design

Review pass 2. Three documents that ship with this PR still described the
constructor, the lib/gbm farm and mcpp_gbm.h as though they existed:

  * docs/descriptor-examples.md and its zh counterpart -- the catalog row is
    the first thing a reader opens when looking for this shape, and it
    described the workaround as the design. Rewritten around what the package
    actually is: it sets nothing, and the backend path comes from
    xim:mesa's declaration. The historical note stays in one clause, because
    "it briefly did carry a constructor, and deleting it took 598 lines to
    303" is the useful part to remember.

  * .agents/docs/2026-08-29-add-libgbm-plan.md -- the first-round design
    record, whose central section is titled "the part that is actual work" and
    is about machinery that no longer exists. Banner at the top rather than a
    rewrite: the shape decision, the zero-host rule, the two-directory-key
    finding and the test design all still hold, and the reasoning that was
    overturned is worth keeping next to what overturned it.

Nothing in the descriptor or the tests changed here; this is the documentation
catching up with the code.

* docs(libgbm): record the final state — the follow-up in 16.4 is done

Section 16 said the constructor could not be deleted until #713 merged. It has,
so 16.3 is marked stale and a new section 18 records what the package actually
ended up as: 598 lines to 303, with the constructor, the backend farm,
mesa_libdir() and mcpp_gbm.h all gone.

Also records what the two test binaries now guard, which is the part worth
knowing: their assertions point OUTSIDE this repository -- at xim-pkgindex's
DISCOVERY row, at xim:mesa still placing its backends, and at mcpp still
injecting subos env. The member stopped being a self-test and became the
ecosystem's tripwire.

Verification in 18.2 is against the PUBLISHED artifact (xlings update), not a
locally patched index copy, which is the distinction section 12.6 warns about.

* feat(graphics): compat.libdrm, compat.egl and compat.wayland — the KMS/DRM
stack closes

compat.libgbm on its own can allocate a buffer and do nothing with it. These
three are the rest of the stack, all on the binding shape libgbm validated:

  compat.libdrm    the layer underneath -- drmModeAddFB2 / drmModeSetCrtc turn
                   an allocated buffer into a scanout
  compat.egl       the layer that makes it renderable --
                   eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_dev, NULL)
  compat.wayland   the other display path -- be a client of, or be, a compositor

WHY BINDINGS AND NOT SOURCE BUILDS. libdrm and wayland both PASS the
separable-unit test that libgbm fails -- independent freedesktop projects with
their own releases, and Conan carries libdrm as a real recipe. They are
bindings for the second criterion instead: the ecosystem already owns them,
Mesa's own payload has DT_NEEDED on both, and a second libdrm.so.2 or
libwayland-client.so.0 in a process that also loads Mesa means two handle
tables for one connection. EGL is the sharpest case: it is a spec, the thing
you link is glvnd's vendor-neutral dispatch library, and building a second one
would be the "one loader per process" mistake compat.vulkan-runtime already
documents.

Three things worth knowing, each found by making it work:

  * libdrm needs TWO include roots. Public headers at the include root, the
    uapi headers they include under libdrm/, and xf86drm.h line 40 is a bare
    `#include <drm.h>`. Expose one root and nothing compiles -- measured while
    writing compat.libgbm's own test, which hit exactly that.

  * compat.egl ships ONLY EGL/, out of a payload that also carries GL/, GLES2/,
    GLES3/ and KHR/. A third provider of GL/ would turn the two-provider race
    compat.glx-headers documents into a three-way one; KHR/ comes from the
    index's existing compat.khrplatform, and that edge is load-bearing rather
    than tidy -- without it EGL/egl.h does not parse. X11 is deliberately not a
    dependency: that include is USE_X11-gated, and forcing Xorg onto headless
    GBM users would be exactly wrong.

  * compat.wayland harvests four libraries and puts only -lwayland-client on
    ldflags. A dependency's ldflags reach the consumer's link line with no way
    to opt out, so forcing libwayland-server on every client would be
    unfixable downstream. A compositor author adds it themselves and it
    resolves out of the farm -- and the test member does exactly that, so the
    documented escape hatch has a regression guarding it rather than a promise.

Verified: all three `mcpp test` green cold via the CN mirror. EGL's client
extension list includes EGL_KHR_platform_gbm / EGL_MESA_platform_gbm, which is
the seam with compat.libgbm; libdrm's DRM_FORMAT_XRGB8888 is asserted equal to
the 'XR24' fourcc compat.libgbm asserts, because those two values cross the
gbm_bo -> drmModeAddFB2 boundary and a mismatch shows wrong colours rather than
an error. Lint + xpkg parse clean across 139 descriptors. CN mirrors published
for all three and re-fetched byte-identical against GLOBAL.
Sunrisepeak pushed a commit that referenced this pull request Aug 31, 2026
`make@4.4` 与 `cmake@3.28` —— 索引里是 make 4.3 与 cmake 4.4.2/4.0.2,两个都从没
解析过。这一点此前不可见,因为 #531 的供给不读自己的结果:xlings 报出的每一种失败都
被当成成功。于是**这条最直接覆盖 `[xlings] deps` 的测试,是建立在一次从未成功的安装
之上做断言的** —— 正是 #531 想要终结的那个状态。

结果被读之后,fixture 的错误第一次可见:构建停下来了。这不是回归,是这条修复第一个
抓到的真实例子,而它抓到的是仓库自己的测试。

改用 `ninja@1.12.1`:mcpp 能构建的地方它一定已装,所以供给短路,这条测试仍然不花
任何下载。
Sunrisepeak pushed a commit that referenced this pull request Aug 31, 2026
e2e 88 的 fixture 声明 `make@4.4` 与 `cmake@3.28`,而索引里是 make 4.3、
cmake 4.4.2/4.0.2 —— 两个版本从来不存在。这条测试在 #531 的整个生命周期里都是绿的,
因为供给不读自己的结果。

⭐ 一般形状:**fixture 里的取值在有东西开始检查它们的那一刻,就不再是随意的了。**
在「这段文字有没有到达那个文件」是唯一断言的时候,它们只是自由字符串。
Sunrisepeak added a commit that referenced this pull request Aug 31, 2026
* fix: #540 的七条审计,以及核验它们时挖出的四条 (2026.9.1.1)

七条里六条成立,一条判据打偏。核验过程本身挖出四条没有人报过的,其中一条比原报告
的全部七条都严重。它们几乎全是同一族:**mcpp 关于自己说了一句话,而 mcpp 不遵守它。**

完整核验、量化与设计见
`.agents/docs/2026-08-31-issue540-seven-audit-findings.md`。

── 1. 供给从不检查自己是否成功(未报告,最严重)────────────────

`xlings::call` 返回 `expected<CallResult, string>`,只要子进程跑起来就处于**值**态
—— 能力自身的状态在 `CallResult` 里面,因为 xlings 讲完 NDJSON 协议后按设计退 0。
#531 的调用点只测了 `if (!r)`,于是 xlings 能报出的每一种失败都被读成成功。实测:

    $ mcpp build                    # deps = ["definitely-not-a-real-package"]
    Provisioning [xlings] deps (definitely-not-a-real-package)
        Finished dev [unoptimized + debuginfo] in 0.12s
    $ cat .mcpp/.xlings-deps.stamp
    definitely-not-a-real-package                   ← 记为已完成
    $ mcpp build
        Finished dev in 0.00s                       ← 连 Provisioning 都不再打印

xlings 报得完全正确(`E_NOT_FOUND` + `{"exitCode":1,"kind":"result"}`),`call()` 也
解析对了。⚠️ 正确写法就在同一个文件里:依赖安装路径写的是
`if (r && r->exitCode != 0 && …)`。#531 的注释说它修的缺陷是「声明看起来被接受了却
什么都没做,这是一个配置键能有的最坏形态」—— 没人读结果,它的修法重现了那个形态,
而记号把它变成永久的。

── 2. 该路径不认两个自动安装开关(未报告)────────────────────

它自称与 `[toolchain]` 平权,而那条先例在 `MCPP_OFFLINE` 或 `MCPP_NO_AUTO_INSTALL`
下硬错并报出触发的是哪一个。⚠️ 一个专门导出 `MCPP_NO_AUTO_INSTALL` 来阻止意外下载
的 CI,会从一条从没听说过这个变量的路径上拿到下载。

拦的是安装**动作**而不是整块:已供给好的工程仍然离线构建得出来。

── 3. 记号记录全局效果却存在项目里(未报告)────────────────

安装落在 registry(刻意如此),而 `<project>/.mcpp/.xlings-deps.stamp` 记着它。清掉
或换掉 `MCPP_HOME`,项目仍然声称已装;`mcpp clean` 只删 `target/`,也清不掉。改按
依赖列表哈希存进 registry,并且只在成功时写。

⚠️ 搬迁不得让昨天能跑的构建今天被拒。自审时发现:升级后每个已供给的工程读起来都是
「未供给」,配上第 2 条的闸,离线首次构建会被拒。旧记号因此在**唯一一处**被采信 ——
就是那道闸 —— 因为在那里网络关着,没有别的办法查证。它绝不被提升进 registry:写它
的那个版本不读结果,所以它的含义是「尝试过」而不是「成功了」,别处采信等于把缺陷
带过修它的这次升级。

── 4. 三份手抄的词汇表,三份都漂移了 ───────────────────────

`kKnownBuildKeys`、`kKnownConditionalBuildKeys` 与 xpkg 的 `target_cfg` 列表,都是
别处已有机器可读形式(紧挨其上的读取点、`BuildInputs` 的成员表)的转录。代价不是
少一条警告,而是**一条假的警告**。

* `[build] std-module` / `std-compat-module` / `std-module-flags` 被读取却报
  unsupported —— `kKnownBuildKeys` 的**第二次**漂移,而第一次的详细叙述就在它上方
  八行。
* 条件轴拒绝 `BuildInputs` 的两个成员:`std-module-flags`(#494 就是为这条轴才把它
  挪上来的,成员注释写着「membership here is what makes the cfg axis carry it」)
  与 `private_include_dirs` —— 后者更严重,xpkg 描述符的 `target_cfg` 块,也就是
  **同一条轴的另一套语法**,是接受它的。
* `[features]` 是唯一一个完全没有 schema 检查的结构化段落。

两条列表的消息现在都由列表本身生成。新增 6 个单测,每个都带否定对照 —— 「没有警告」
这类断言会被一个把检查整个删掉的解析器满足。

── 5. cfg(<层> = "…"):文档记载而从未接线的特性 ────────────

docs/14 用一整节记载它,连「为什么不能用 feature 选择代替」和作用域约束都论证过;
而 `cfgpred::Ctx` 只由三元组构造,`match_kv` 只认 os/arch/family/env。于是每一个这样
的段落被**静默**丢弃,包成功构建在错误的 C 库配置上。实测:`cfg(env="gnu")` 生效、
`cfg(c-abi="glibc")` 不生效、零诊断。8 处文档如此(中英各 4)。

实现:目标侧解析(`tsd::resolve`)与 P1689 扫描之间有一段空窗,而 build.mcpp 已经在
用它 —— 它按同样的形状把 directive tail 镜像进 `packages[0]`。第二趟合并用同样的
`directives::mark` + `fold_private_tail`,不另造机制。

⚠️ 两趟必须不相交,而只靠 `matches()` 做不到:`cfg(any(linux, c-abi="musl"))` 的
三元组腿在第一趟就为真,第二趟会再匹配一次,`append()` 是追加式的于是贡献两遍。
按**是否命名了层**归属,而不是按答案。e2e 328 数 `-D` 出现次数来守这条。

⚠️ 层谓词不能选择依赖(层是从依赖图解析出来的),这种段落被报出并忽略。

── 6. 未知的 cfg 键现在会说话 ──────────────────────────────

求值器过去对未知键返回假,而那与「这一段本就不该匹配」读数完全相同。⚠️ 词汇表从
求值器**导出**而不是被转录 —— 否则这条诊断自己就会成为第 4 条里的第四份手抄件。
求值器同时就是校验器:一次遍历回答三个问题,因为另写一个校验器就是同一份文法的
第二个解析器,而本仓库已经为其中一个付过账。

`ident()` 现在接受 `-` 与 `+`,否则 `c-abi` 会被扫成裸词 `c` 加一堆垃圾,诊断能报的
就只有字母 `c`。

── 7. c-abi 层报的是库名,不再是三元组的 env 段 ──────────────

这条是实现第 5 条时才暴露的:谓词是一次比较,而比较有两侧,而此前的设计工作从没问过
右侧的取值是什么。它在普通 Linux 宿主上是 `gnu`(`payload_libc_name` 原样返回 env
段),而 docs/14 的表一直写着 `glibc`/`musl`/`picolibc`,e2e 296 的文件头也把它期望的
报告写作 `c-abi glibc (payload)`。⚠️ **一个只被打印的值没有拼写纪律,把它提升为用户
比较的对象会追溯地强加一条。**

请求侧保留三元组的拼写(规范 §3.4:env 段是对 c-abi 的请求而非答案),两者经
`c_abi_request_satisfied` 比较而非按相等 —— 否则每一次普通 `-gnu` 构建都会被报成请求
不匹配。Windows 上 `-gnu` 命名的是工具链的 MinGW 形态,其 C 运行时是 UCRT,因此映射
按 OS 分叉。

── 8. 退出码:补上 runtime 的一半,并写下被指派的契约 ─────────

原报告说 docs/11 的表漏了 `4`。判据打偏了:那张表按信封命令划定,而**没有一个信封
命令给得出 4** —— `self env --format json` 恰恰是被特意做成绕开产生 4 的
`load_or_init` 的。表真正漏的是 `1`(`xpkg parse` 五处返回)。

2026-08-08 的协议设计文档 §R4 把完整契约指派给了 `docs/spec/`,一直没有写。现在写了:
`docs/spec/exit-codes.md`(SPEC-003),0/1/2/4/70/127 全表 + 稳定性承诺。

── 9. 其余文本 ─────────────────────────────────────────────

* `mcpp build --help` / `mcpp test --help` 说默认档位是 release,而它是 dev。六处说得
  对(含一条 e2e 与 mcpp 自己的 mcpp.toml),两处说错;`prepare.cppm` 那条字段注释是
  没被报告的第三处。
* `mcpp index update <name>` 承诺按索引筛选而只筛项目级。限制此前只写在一条注释里 ——
  一个只有实现者看得到的地方,从外面看与「这功能坏了」无从区分。
* docs/13 与 docs/17 仍在说 `[xlings] deps` 不是安装触发器(#531 之后为假)。
* `mcpp::target_libc()` 的文档改为它实际回答的问题:供给 sysroot 的那个**载荷**包,
  而这个值是目标侧解析的一项**输入**。

── 测试 ────────────────────────────────────────────────────

单元:test_manifest 新增 6 个(三份词汇表各一正一负),test_targetside 新增 2 个;
96 个测试二进制全过。

e2e:新增 327(供给失败会报出来 + 两个开关 + 搬迁连续性,五条断言,前两条不需要网络)、
328(层谓词生效/不生效/恰好一次 + 未知键 + --strict)、329(退出码契约,含「退 1 且
stdout 带信封」)。

⚠️ 判据的分母:327 的核心断言跨**两次**调用 —— 为失败而写的记号在写它的那一次里
不可见,只有第二次构建才分得开「失败了」与「失败了并被记成完成」。328 数 `-D` 的
出现次数而不是用预处理器判断,因为预处理器分不开一个 `-D` 和两个。

* fix(provisioning): stamp key uses uint64_t, not size_t

一个 32 位宿主会把 64 位的 FNV offset basis 截断,于是它拥有一份与别人不同的键空间
而没有任何东西说明为什么。碰撞本身两侧都不是正确性问题 —— 记号文件存的是**列表**,
比较也是针对内容的,所以两个共键的列表会重新供给而不是悄悄采用对方的记录。

* refactor(provisioning): 闸不再多套一层缩进

把 `have != want` 换成一个 `needProvision` 布尔,于是自动安装闸可以在供给块**之前**
求值并在采信旧记号时把它关掉 —— 供给块本身保持原来的嵌套层级。行为不变;改的是让
一个千行的 PR 里这一段仍然读得下去。

* fix(features): 保留键的诊断指向一个存在的拼写

自审读出:`deps` 的那条消息提供了 `optional = true`,而 mcpp 从来没有这个键。
一条把读者送去一个解析器不认识的键的诊断,与本次发布正在移除的那些警告是同一个
缺陷,只是外了一层。文档化的机制是 `[feature-deps.<name>]`(docs/05 §2.8.2)。

同步中英两份 docs/05。

* test(e2e 328): 补上依赖那一条腿 —— 它才是这个特性的动机

自审读出:328 此前每一条断言都能被一个只给 packages[0] 打补丁的实现满足。而
docs/14 是为「供给某一层、并支持其下方多个实现的包」写的这个特性 —— 那是一个**库**,
以别人的依赖身份被走到。与本 pass 共用同一段窗口的 build.mcpp tail 只打补丁给根包
(对它自己的用途是对的),照抄那个形状会让这个特性唯一存在的对象没被服务,而所有
只看根包的断言照样全绿。

新增的这条带否定对照:依赖里不匹配的那一段必须不生效。

* docs(11): 记下 layers[].interface 的取值变化

它是机器接口上的一个字段,而取值从 `gnu` 变成了 `glibc`/`ucrt`。§6 承诺的是
字段的**含义**不变 —— 含义确实没变 —— 但按字面量取值的客户端会受影响,而契约页
不说这件事,就只能靠对方撞上。

* test(e2e 328): 层谓词不能选依赖 —— 补上这条的判据

实现了这条诊断,却从没跑过它 —— 而「判据写了、绿了、却从没跑到」正是本次发布在修的
那一族。带一条同样重要的对照:同一个谓词下的 build 输入必须**仍然生效**,否则一条
警告就是把一次静默丢弃换成了另一次。

* test(e2e 88): fixture 声明了两个从不存在的版本

`make@4.4` 与 `cmake@3.28` —— 索引里是 make 4.3 与 cmake 4.4.2/4.0.2,两个都从没
解析过。这一点此前不可见,因为 #531 的供给不读自己的结果:xlings 报出的每一种失败都
被当成成功。于是**这条最直接覆盖 `[xlings] deps` 的测试,是建立在一次从未成功的安装
之上做断言的** —— 正是 #531 想要终结的那个状态。

结果被读之后,fixture 的错误第一次可见:构建停下来了。这不是回归,是这条修复第一个
抓到的真实例子,而它抓到的是仓库自己的测试。

改用 `ninja@1.12.1`:mcpp 能构建的地方它一定已装,所以供给短路,这条测试仍然不花
任何下载。

* docs: 记下 D12 的第一个捕获对象是本仓库自己的测试

e2e 88 的 fixture 声明 `make@4.4` 与 `cmake@3.28`,而索引里是 make 4.3、
cmake 4.4.2/4.0.2 —— 两个版本从来不存在。这条测试在 #531 的整个生命周期里都是绿的,
因为供给不读自己的结果。

⭐ 一般形状:**fixture 里的取值在有东西开始检查它们的那一刻,就不再是随意的了。**
在「这段文字有没有到达那个文件」是唯一断言的时候,它们只是自由字符串。

* docs(spec-003): 穷举核对写成读数,而不是「我数了一遍」

§4 原本给了一个退出码清单并声称穷举,而那个清单少了三处 `return 4`(pack/pipeline、
cmd_toolchain、pm/commands —— 都是同一个 `load_or_init` 失败,所以 `4` 的含义没变,
共 11 处而非 8 处),也没提 `20` 与 `1024`。

改成:贴出产生读数的命令,然后**按出处**逐个归类。⚠️ `4` 同时落在两栏 —— 它在
`index_management.cppm` 里是退出码,在 `runtime/elf.cppm` 里是 `R_RISCV_COPY`。
一份声称穷举的规范,自己就得能被复核。

* test(e2e 328): 两处只在 Windows 上才成立的问题

⚠️ **`cfg(all(unix, …))` 在 Windows 上正确地为假**,于是 fixture 的 #error 触发,
Windows e2e 1/2 变红。这条腿要证明的是「三元组键与层键**组合**」,那么它的三元组
那一半就必须在测试会跑的每个平台上为真。改成 `any(unix, windows)`。
「恰好一次」那条腿同理:三元组腿为假的地方,只有一条路能匹配,这个守卫就不再守任何
东西 —— 它存在的理由正是第一趟会经三元组腿匹配而第二趟经层腿匹配。

⚠️ **注释里的反引号落在**未加引号**的 heredoc 里,变成了命令替换。** fixture 要
插值 $CABI 所以 heredoc 不能加引号;套件因此打印
`syntax error: unexpected end of file` 而**测试照样通过**。注释移到 heredoc 之外。

顺带:`-DPROBE_ONCE=1` 的计数改为同时接受 `/D`(Windows 自举可能驱动 MSVC)。

本机全量:355 passed / 3 failed —— 三条(62、168、208)在已发布的 2026.8.30.2 上以
相同消息失败,是本机 musl/glibc 载荷的缺口,对照已跑。

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sunrisepeak pushed a commit that referenced this pull request Sep 3, 2026
Three facts shape it. xlings auto-activates only when nothing is active for
that name in the merged view, so a version active globally makes a project's
declaration lose. Activation writes into the project's own SubOS layer, which
config.cpp places under the project directory, so forcing it disturbs no other
checkout and not the machine. And the two value spellings ask for different
things: a version wants activation, an empty value wants presence.

The deciding fact is the fourth. When a forced install fails to switch, the
installer logs a warning and the call still exits zero, so useAfterInstall
gives mcpp a request whose outcome it cannot read — #531 from the other side.
use_version returns cmd_use's exit code instead.

Hence the shape: one unforced install batch, then one use_version per versioned
entry with its exit code checked, nothing extra for unconstrained entries, and
the existing stamp covering both. Six criteria, one of which must be seen
failing first.
Sunrisepeak added a commit that referenced this pull request Sep 3, 2026
…moved, and the install-time edge published (2026.9.3.1) (#548)

* design: [xlings] converges on one table, and deps is retired

deps and workspace state the same thing about a project and differ only in what
mcpp does with the statement. The document proposes that workspace become the
one table, records the two facts that make the split indefensible — the general
form installs nothing while its own documented shorthand installs, and nothing
compares the two when both name one package — and carries the deprecation path,
the packaging map that does not exist, and the loss the per-platform resolution
introduced for it.

Design only. Five questions in section 11 are xlings' to answer and are stated
rather than assumed.

* design: settle what is written, and state the section it is being changed against

Section 4 is decided rather than open: the correspondence is one to one in name
and in meaning, so the merged table materialises as the file's own workspace
object and no deps array is derived. The decision carries a consequence for W2
— mcpp appends the target's C library to that channel itself, as a namespaced
reference with a version, so a key that cannot hold a namespace would leave
mcpp unable to express its own entry.

Section 12 records the section as a whole: field correspondence, the three
places mcpp is not a pure mirror, the ownership rule and where the file is
written, subos in full, and the two environment channels that are easy to
confuse.

* design: what the four fields do, read in the xlings source

Section 13 answers section 4 and Q1 from the implementation rather than from
inference, and corrects two claims this document and docs/05 both made.

xlings does not provision from workspace: deps is the only key any install path
reads, so the merge belongs in mcpp.toml and the file keeps both fields — one
statement materialised into the two places xlings reads its two halves.

envs has no reader on either side. Every consumer in the xlings source is
either xvm's per-program VData::envs or a SubOS's subos_info provider sections,
and neither is the flat object mcpp writes; mcpp's own run environment comes
from the runtime binding. The key is documented as having an effect it does not
have, and the section proposes deciding its fate before this proposal ships.

Also recorded: a named subos drops the global workspace layer entirely, which
is a property of subos that nothing in mcpp states today.

* design: state the inheritance rule in full, not only the case that surprises

The previous wording named the layer a declared subos drops and not the two
rows where inheritance is what an author would expect. A project that pins a
few tools and declares no subos starts from the global workspace, which is the
common shape; a named subos inherits its own environment's workspace instead,
because carrying the host's pins into a different installed set would name
versions that are not there; and a fork copies its base's map once at creation.

Also records that nothing falls back silently: with no layer naming a tool the
shim diagnoses rather than choosing, which is the line the 2026.9.2.1 sandbox
verification measured.

* design: the documentation the rule needs, and a self-review that changes the proposal

Section 14 assigns the inheritance rule to docs/05 §2.13 and docs/17 with their
Chinese twins, as a table plus the reason the middle row is not an omission,
and gives it a criterion so the paragraph is not the only record.

Section 15 is the second read against the code. Its first finding changes the
proposal: a project-file workspace entry is merged verbatim and the shim looks
up a program's own name, so naming a package root pins a node nothing invokes
while its programs resolve from the layer underneath. Group expansion happens
in cmd_use, which a manifest never goes through. Three ways out are stated;
expanding at read time is recommended and is xlings' to accept.

Two smaller corrections: 'retired' has always meant the manifest key and never
the file field, and section 12.6's claim to touch neither subos nor envs is
withdrawn.

* design: four questions answered from the source, and 15.1 withdrawn

15.1 claimed a manifest workspace entry cannot pin a package's programs. Both
its premises hold and the conclusion does not: the expansion happens in the
action mcpp triggers, not in the merge. Installing calls cmd_use, cmd_use
resolves the release and writes one entry per member, and with a project config
loaded those land in the project's own SubOS layer.

What the read did find is one flag. Activation after install happens only when
nothing is active yet, so on a machine already holding another version the
declared one is installed and not run. The capability already takes
useAfterInstall; D8 sends it.

W1 and W2 are rewritten against the file format rather than inferred: stored
values are always concrete, latest and a version prefix are the input
spellings, and every key in three real subos files is a bare target name with
no namespace. envs is decided — retired, with the documentation correction
shippable on its own.

* design: the key space measured, the namespace put where the file puts it, and the migration collapsed to one release

Three corrections, all from data rather than inference.

A workspace key is an xvm target of any kind. The default SubOS holds binutils
beside ar, as and ld at one version, and gcc beside g++ and cc at another:
package roots, programs and file assets share one namespace, and a root and its
programs agree because cmd_use wrote them as one release. Calling the keys
program names was wrong, and writing a package in a manifest is a legitimate
entry whose programs receive the same version.

The namespace rides the value, not the key: parse_ns_version splits the first
colon of a version string and a real entry reads xim:2026.8.30.2. So the form
is picolibc-riscv = "xim:1.8.12", which needs no rule mcpp has to invent.

The migration is one release. Three manifests declare the key and mcpp declares
none, so the deprecation window buys nothing; what does not bend is that deps
is refused with a message rather than dropped in silence.

* design: the key form checked against the data, and ns:name rejected with its reason

W2 asserted where the namespace goes; it now measures. 1635 version-database
targets and 546 workspace entries on the development host, and not one key
contains a colon.

Two reasons ns:name cannot be the key, and neither is convention. A workspace
key is looked up by the name a program is invoked as, and nothing is invoked as
xim:picolibc-riscv, so the key would be read by nobody. And the namespace
qualifies where a version came from rather than what the tool is: mcpp itself
holds eight versions under one target, some scoped xim: and some not, which
moving the namespace onto the key would split into two tools with one name.

A colon in a key is therefore a hard error naming the correct form, not a
second accepted spelling. The section 3 example is corrected in the same pass:
the invented "*" becomes latest.

* design: the authored project file is the reference, not a SubOS state file

Two artifacts share the name and this document measured one for the other. The
authored file maps a name to a version string or a platform object;
d2mcpp/.xlings.json is the published example, and it settles two of the three
open items.

Version unconstrained is the empty string. "code": "" is already in use, the
resolver returns it unchanged, and an empty value reads downstream as claiming
no version — which is what deps = ["cmake"] means today. No * and no latest is
introduced.

The namespace in the key position is undefined rather than forbidden: nothing
derives an install address from a workspace entry, because nothing installs
from workspace at all, so the question becomes real only when mcpp does. Both
candidate forms are stated with what each costs, and the item is marked as a
request rather than a finding.

The native platform keys are recorded as linux, windows, macosx and default.

* design: both namespace positions accepted, and mcpp normalises to the file's own

W2 was a request; it is now a decision. An author may put the namespace on the
version or on the key, because the two are interconvertible and neither is more
natural than the other — one matches how deps spells a package, the other
matches what the file already contains.

Four rules keep it one fact: mcpp materialises the file's own form only, the
install address is assembled from whichever half carried the namespace, stating
it twice and differently is a hard error, and the key form needs quotes.

The quoting is measured rather than asserted. TOML bare keys are [A-Za-z0-9_-]
and mcpp's lexer matches; on the 2026.9.2.1 binary the unquoted key fails at
the colon with a parser message that mentions nothing about namespaces, so the
documented example carries the quotes.

The previous rule making a colon in a key an error is withdrawn.

* design: D8 worked through — activation must be obtained, not requested

Three facts shape it. xlings auto-activates only when nothing is active for
that name in the merged view, so a version active globally makes a project's
declaration lose. Activation writes into the project's own SubOS layer, which
config.cpp places under the project directory, so forcing it disturbs no other
checkout and not the machine. And the two value spellings ask for different
things: a version wants activation, an empty value wants presence.

The deciding fact is the fourth. When a forced install fails to switch, the
installer logs a warning and the call still exits zero, so useAfterInstall
gives mcpp a request whose outcome it cannot read — #531 from the other side.
use_version returns cmd_use's exit code instead.

Hence the shape: one unforced install batch, then one use_version per versioned
entry with its exit code checked, nothing extra for unconstrained entries, and
the existing stamp covering both. Six criteria, one of which must be seen
failing first.

* design: D8 restated — the flag was the wrong question, the scope is the right one

Correction first: provisioning does not write into the project's layer. It
calls with make_xlings_env, which carries no projectDir, so the child runs in
global scope against mcpp's registry home. The earlier claim that forcing was
safely contained is withdrawn, and three measured facts follow from the real
scope, including why the runner needed a payload lookup at all.

xlings already implements both halves of the requirement: an existing payload
is mapped rather than reinstalled, and install activates whenever nothing is
active for that name. Whether mcpp receives that behaviour depends on the
environment it asks in, not on a flag.

Design P provisions in project scope, where the project's own SubOS is the
layer written; with a named SubOS the global layer is not merged, so the
declaration is what activates and no forcing is needed. Design G keeps the
global scope and forces, which lets two checkouts fight and cannot be verified.

P is recommended, and it carries one question that is not mcpp's alone: whether
a project environment inherits the machine's. Both branches are stated with
what they cost.

* design: D8 withdrawn — the merge already does it, and the defect is the provisioning scope

merge_workspace_into_ assigns rather than inserts, so the later layer wins: in
Anonymous mode the project's declaration beats the machine's, and in Named mode
the machine's layer is not merged at all. A project declaring a version
therefore resolves to it without anything forcing a switch, and the developer
picks the strength by naming a SubOS or not.

What is wrong today is one argument. Provisioning calls with make_xlings_env,
which carries no projectDir, so the child runs in global scope: the install's
records land in the registry's shared workspace while programs invoked from the
project resolve through the project's layers. That disagreement is the
'not installed in this subos' line the 2026.9.2.1 verification measured and the
reason the runner needed a payload lookup.

So the change is make_project_xlings_env, and useAfterInstall is not passed and
no use_version call is added. Two consequences are written down rather than
left to be discovered: the project SubOS layer outranks the manifest, and an
uninstalled declared version already fails with wording that names the project.

* design: rewritten as one document, and the scope fix's precondition checked

The document had accumulated seven contradictions across the review rounds: the
summary described a deprecation path the migration section had replaced with a
single release, the open-questions section still named a decision a later
section had withdrawn, and the axes referred to phases that no longer exist.
Rewritten rather than patched, 851 lines to 493, with section 15 keeping every
abandoned position and why it was abandoned.

The framing is the one the review arrived at: [xlings] is mcpp's manifest
surface for xlings' local project mechanism, and each decision is an answer to
what that mechanism already does.

Added a precondition the scope fix depends on and that nothing else would have
caught: xlings refuses to treat a directory holding a bare subos/ as a project,
and a project's SubOS lives one level deeper, so .mcpp qualifies today —
measured — but a future bare subos/ under it would make mcpp's own project file
invisible everywhere at once. C13 asserts it on an effect, because a skip
degrades to today's behaviour and would look like success.

* design: the migration corrected against the consumer, and an implementation plan

Step 1 of the migration refused deps everywhere. That counts the wrong
denominator: the three manifests that declare the key are consumed, and a
consumer pins an exact version, so a refusal reaching a dependency's manifest
makes riscv-virt-rt@0.6.0 unbuildable for everyone who pinned it while no
republished version reaches them. The refusal applies to the root manifest,
where the author can act on it; a dependency's is honoured and reported once.

Section 16 splits the work into eight tasks with their dependency order, and
states what each review axis demands of the implementation rather than of the
design.

* xlings: one table, deps superseded, envs removed, and the install-time edge published

`[xlings]` is mcpp's surface for xlings' local project mechanism, and this
brings the manifest into line with what that mechanism has.

`[xlings.workspace]` becomes the one table. An entry names a package and the
version this project uses it at, and produces two projections: an install
address for provisioning and a resolution pin for the materialised
`.xlings.json`. The namespace may be written on the key or on the version —
`"xim:picolibc-riscv" = "1.8.12"` and `picolibc-riscv = "xim:1.8.12"` are one
entry — because both spellings already exist in the ecosystem and neither is
more natural; writing it on both halves with different values is refused, as is
naming one package twice. `""` means present with the version unconstrained,
which is the spelling an authored project file already uses. Platform keys are
xlings' own, with `macos` kept as an alias.

`deps` is superseded and still honoured, reported once with the line to write
instead. It is not refused: a refusal would reach a dependency's manifest that a
consumer pinning that package cannot edit. One package in both tables with two
versions is a hard error, because the two are provisioned in order while the
last wins the pin — install one, resolve the other.

`envs` is removed. It was materialised into the file and read by nothing: the
two `envs` structures xlings has belong to a program's own shim record and to a
SubOS's provider sections, and mcpp's run environment comes from the runtime
binding. The documentation described an effect that did not occur, which is why
this is an error rather than a warning.

`mcpp emit xpkg` writes `xpm.<platform>.deps`. Nothing emitted it before, so a
package declaring an environment had its install-time edge written by hand —
which is how riscv-virt-rt 0.3.0 shipped without the C library its own target
row named. The declaration is kept unresolved per platform for it, since a
descriptor has a block per platform and the host resolution has discarded two.

Documented: the three-row inheritance rule, which has always held and which
mcpp stated nowhere — no `subos` means the machine's environment with the
project's entries over it, a named one means isolation, and an `xlings use`
inside the project outranks both.

Verified: e2e 331 asserts on the addresses the provisioning pass names with
auto-install off, so every criterion runs on every shard with no network; it
fails at its first assertion on 2026.9.2.1. Unit: test_manifest +8. Full unit
suite 97 passed; e2e 06, 93, 130, 131, 132, 134 re-run.

* manifest: one address parser, and the conflict compared on the whole pin

The deps compatibility path derived (namespace, target, version) from an
address twice — once to compare against a workspace pin and once to phrase the
advisory — which is the shape where two copies of one rule drift. parse_address
is the inverse of XlingsEntry::address, and both sites read it.

The comparison also widened. It tested versions alone, so 'other:foo@1.0'
beside 'foo = "xim:1.0"' passed as agreement: two packages sharing a name at
one version. Comparing the pin covers both halves.

* design: state where the advisory does not reach

It rides schemaWarnings, which prepare prints for the root manifest and
escalates under --strict. A dependency's are attached and not printed, which is
a pre-existing gap: surfacing them would also surface every unrelated warning
those manifests carry, and that is a change of its own. So the package's author
is told the moment they build it, and a consumer of an unmigrated package is
not — stated here rather than left to be discovered.

* design: the ecosystem migration waits, and the reason is a silent regression

Republishing the three packages with the new table before their consumers move
would stop provisioning on every older engine, silently: an older mcpp parses
[xlings.workspace] and provisions nothing from it. That is worse than the
advisory it would silence. This cycle verifies the three packages on the new
engine instead; the republish waits for a floor that makes the older engine
refuse rather than degrade.

* docs(13): the board-support example uses the one table

The chapter taught [xlings] deps as the way a board package declares its
emulator. The declaration is the same statement; only the table changed, and
the older spelling still works and says so.

* docs+publisher: the subos example was nested under the wrong table, and deps emits no edge by design

The 2.13 example put `subos = "dev"` after `[xlings.workspace]`, which in TOML
makes it `xlings.workspace.subos` — a key nothing reads. Two blocks, each under
the table it belongs to.

The emitter gains the reason it skips a package still on `deps`: that key is
host-resolved at load, so the per-platform declaration a descriptor needs is
already gone, and writing the host's answer into all three blocks would be the
machine-dependent descriptor the function exists to avoid.

* tests: the fixtures that used the removed key, and the one assertion only macOS could fail

Three CI-only failures, all from this change and each in a fixture rather than
in the engine.

e2e 88 and 205 declared [xlings.envs], which is now refused. 205 drops it; 88
asserted the key was materialized and now asserts it is not, and gains two
criteria the merge introduced: the same statement in both tables collapses to
one entry, and the superseded key is reported with the line to write.

The manifest parser gained that collapse. deps and workspace agreeing about one
package is not an error and is not two entries — appending it twice would ask
xlings to install one package twice.

Manifest.XlingsWorkspaceAcceptsPerPlatformValues compared host_platform_key()
against "macos", and this change made that key xlings' own spelling, "macosx".
The assertion was written in terms of the function precisely so it would run on
every host, and it was the one line in the file that could only fail on a macOS
runner. It did.

* examples: the project-subos example writes the one table

It declared its emulator with [xlings] deps, which now reports itself. The
address the new form assembles is byte-identical — measured with auto-install
off, which prints what the pass was going to ask for:
'xlings install xim:qemu-riscv@9.2.4-1' either way.

* design: what implementation found that the design did not

Four items, three visible only once the code ran: a nested std::map in an
exported module truncating the BMI and reporting it in an unrelated file; the
removed key's first casualties being this repository's own fixtures; one
assertion that could only fail on macOS doing so; and the same statement in
both tables collapsing rather than installing one package twice.

* platform: one macos/macosx spelling rule, and the manifest asks it

Two names for one platform: mcpp's triple vocabulary says macos, an xpkg
descriptor and xlings' project file say macosx. The manifest parser had a
second copy of that fold and a second copy of the host key, both hand-rolled
beside a rule modules/platform already had.

mcpp.platform.axis gains xpkg_platform_key_for, which is now the only place
that knows, and TargetPlatform::for_os is written in terms of it rather than
repeating the table. The manifest reads it for both directions — canonicalising
a written key and folding a requested platform — and takes the host from
xpkg_platform instead of its own #if.

Both spellings are therefore accepted wherever a platform is named, and a new
unit test asserts that the two write the same declaration on every host by
resolving explicitly rather than against the one it runs on.

* design: the macos alias is one rule, and mcpp already had it

W3 said mcpp accepts the alias; it did not say where the fold lives. The
implementation lifted it into mcpp::platform::xpkg_platform_key_for, which
TargetPlatform::for_os and the manifest parser both call, and takes the host
key from xpkg_platform rather than a second #if — an earlier draft hand-rolled
both, which would have been a third copy of a two-line table.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants